#!/bin/sh
#-------------------------------------------------------------------------+
# Copyright (C) 2016 Matt Churchyard (churchers@gmail.com)
# All rights reserved
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted providing that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

# 'vm datastore list'
# show configured datastores
#
datastore::list(){
    local _format="%-15s %-11s %-25s %s"
    local _name _type _dataset _path _spec

    # headings
    printf "${_format}\n" "NAME" "TYPE" "PATH" "ZFS DATASET"

    # add the default datastore
    _name="default"
    _path="${vm_dir}"

    # get the type and path
    if [ "${VM_ZFS}" ]; then
        _type="zfs"
        _dataset="${VM_ZFS_DATASET}"
    else
        _type="directory"
        _dataset="-"
    fi

    printf "${_format}\n" "${_name}" "${_type}" "${_path}" "${_dataset}"

    for _name in ${VM_DATASTORE_LIST}; do
        [ "${_name}" = "default" ] && continue

        config::core::get "_spec" "path_${_name}"
        [ -z "${_spec}" ] && continue

        if [ "${_spec%%:*}" = "zfs" ]; then
            _type="zfs"
            _dataset="${_spec#*:}"
            datastore::__resolve_path "_path" "${_spec}"
        elif [ "${_spec%%:*}" = "iso" ]; then
            _type="iso"
            _path="${_spec#*:}"
            _dataset="-"
        elif [ "${_spec%%:*}" = "img" ]; then
            _type="img"
            _path="${_spec#*:}"
            _dataset="-"
        else
            _type="directory"
            _path="${_spec}"
            _dataset="-"
        fi

        printf "${_format}\n" "${_name}" "${_type}" "${_path}" "${_dataset}"
    done
}

# 'vm datastore add'
# create a new datastore
#
# we don't try and create directories or datasets.
# the user should do that first
#
# @param string _name datastore name
# @param string _spec specification (either /path or zfs:dataset)
#
datastore::add(){
    local _name="$1"
    local _spec="$2"
    local _mount _num=0 _curr

    [ -z "${_name}" -o -z "${_spec}" ] && util::usage
    util::check_name "${_name}" || util::err "invalid datastore name - '${_name}'"

    # check name not in use
    for _curr in ${VM_DATASTORE_LIST}; do
        [ "${_curr}" = "${_name}" ] && util::err "datstore '${_name}' already exists!"
    done

    # look for zfs
    if [ "${_spec%%:*}" = "zfs" ]; then
        # try to find mountpoint
        _mount=$(mount | grep "^${_spec#*:} " |cut -d' ' -f3)
        [ -z "${_mount}" ] && util::err "${_spec} doesn't seem to be a valid, mounted dataset"
    else
        # make sure it's a directory
        [ ! -d "${_spec}" ] && util::err "${_spec} doesn't seem to be a valid directory"

        _mount="${_spec}"
    fi

    # see if this is already our default datastore
    [ "${_mount}" = "${vm_dir}" ] && util::err "specified path already exists as default datastore"

    # save
    config::core::set "datastore_list" "${_name}" "1"
    config::core::set "path_${_name}" "${_spec}"
    [ $? -eq 0 ] || util::err "error saving settings to configuration file"
}

# remove a datastore
# we don't actually delete anything, just remove from config
#
# @param string _name name of dataset
#
datastore::remove(){
    local _name="$1"
    local _ds _found

    [ "${_name}" = "default" ] && util::err "cannot remove default datastore"

    for _ds in ${VM_DATASTORE_LIST}; do
        [ "${_ds}" = "${_name}" ] && _found="1"
    done

    # found the dataset?
    [ -z "${_found}" ] && util::err "unable to locate the specified dataset"

    config::core::remove "datastore_list" "${_name}"
    config::core::remove "path_${_name}"
    [ $? -eq 0 ] || util::err "error removing settings from configuration file"
}

# get the filesystem path for the specified dataset spec
# this can either be just a path, or ZFS dataset
#
# @param string _var variable to put path into
# @param string _spec the path spec (either directory or zfs:dataset)
# @return non-zero on error
#
datastore::__resolve_path(){
    local _var="$1"
    local _spec="$2"

    if [ "${_spec%%:*}" = "zfs" ]; then
        _mount=$(mount | grep "^${_spec#*:} " |cut -d' ' -f3)

        if [ -n "${_mount}" ]; then
            setvar "${_var}" "${_mount}"
            return 0
        fi
    elif [ "${_spec%%:*}" = "iso" ] || [ "${_spec%%:*}" = "img" ]; then
        setvar "${_var}" "${_spec#*:}"
        return 0
    else
        if [ -d "${_spec}" ]; then
            setvar "${_var}" "${_spec}"
            return 0
        fi
    fi

    setvar "${_var}" ""
    return 1
}

# load list of datastores into VM_DATASTORE_LIST
#
# @modifies VM_DATASTORE_LIST
#
datastore::load(){
    config::core::get "VM_DATASTORE_LIST" "datastore_list"
    VM_DATASTORE_LIST="default${VM_DATASTORE_LIST:+ }${VM_DATASTORE_LIST}"
}

# init global settings for a vm
# we take a guest name and try to find it in all
# datastores. we don't allow duplicate names, and
# if there is, we will just return the first found.
#
# @param string _guest guest name
# @return non-zero on error
# @modifies VM_DS_NAME VM_DS_PATH VM_DS_ZFS VM_DS_ZFS_DATASET
#
datastore::get_guest(){
    local _guest="$1"
    local _ds _spec _path _found _zfs _dataset

    # look in default store
    if [ -f "${vm_dir}/${_guest}/${_guest}.conf" ]; then
        _found="1"
        _ds="default"
        _path="${vm_dir}"
        _zfs="${VM_ZFS}"
        _dataset="${VM_ZFS_DATASET}"
    fi

    # look on other datastores
    if [ -z "${_found}" ]; then
        for _ds in ${VM_DATASTORE_LIST}; do
            [ "${_ds}" = "default" ] && continue

            config::core::get "_spec" "path_${_ds}"
            if [ "${_spec%%:*}" = "iso" ] || [ "${_spec%%:*}" = "img" ]; then
                continue
            fi

            datastore::__resolve_path "_path" "${_spec}"

            if [ -f "${_path}/${_guest}/${_guest}.conf" ]; then
                [ "${_spec%%:*}" = "zfs" ] && _zfs="1" && _dataset="${_spec#*:}"

                _found="1"
                break
            fi
        done
    fi
    
    # make sure we have a path
    [ -z "${_found}" ] && return 1

    # set variables
    VM_DS_NAME="${_ds}"
    VM_DS_PATH="${_path}"
    VM_DS_ZFS="${_zfs}"
    VM_DS_ZFS_DATASET="${_dataset}"
}

# get the path and details for a datastore
# put into same variables as datastore_get_guest
#
# @param string _ds datastore name
# @return non-zero on error
# @modifies VM_DS_PATH VM_DS_ZFS VM_DS_ZFS_DATASET
#
datastore::get(){
    local _ds="$1"
    local _spec _path _zfs _dataset

    # check for default
    if [ "${_ds}" = "default" ]; then
        VM_DS_NAME="default"
        VM_DS_PATH="${vm_dir}"
        VM_DS_ZFS="${VM_ZFS}"
        VM_DS_ZFS_DATASET="${VM_ZFS_DATASET}"
        return 0
    fi

    config::core::get "_spec" "path_${_ds}"
    [ -z "${_spec}" ] && return 1

    # skip iso and img stores
    if [ "${_spec%%:*}" = "iso" ] || [ "${_spec%%:*}" = "img" ]; then
        return 1
    fi 

    datastore::__resolve_path "_path" "${_spec}" || return 1
    [ "${_spec%%:*}" = "zfs" ] && _zfs="1" && _dataset="${_spec#*:}"

    # set variables
    VM_DS_NAME="${_ds}"
    VM_DS_PATH="${_path}"
    VM_DS_ZFS="${_zfs}"
    VM_DS_ZFS_DATASET="${_dataset}"
}

# get the path for an iso datastore
#
# @param string _ds datastore name
#
datastore::get_iso(){
    local _ds="$1"

    # default?
    # we use the .iso subdir in that case
    if [ "${_ds}" = "default" ]; then
        VM_DS_PATH="${vm_dir}/.iso"
        return 0
    fi

    config::core::get "_spec" "path_${_ds}"
    [ -z "${_spec}" ] && return 1

    # should be an iso ds
    [ "${_spec%%:*}" = "iso" ] || return 1

    datastore::__resolve_path "_path" "${_spec}" || return 1
    VM_DS_PATH="${_path}"
}

# get the path for an img datastore
#
# @param string _ds datastore name
#
datastore::get_img(){
    local _ds="$1"

    # default?
    # we use the .img subdir in that case
    if [ "${_ds}" = "default" ]; then
        VM_DS_PATH="${vm_dir}/.img"
        return 0
    fi

    config::core::get "_spec" "path_${_ds}"
    [ -z "${_spec}" ] && return 1

    # should be an img ds
    [ "${_spec%%:*}" = "img" ] || return 1

    datastore::__resolve_path "_path" "${_spec}" || return 1
    VM_DS_PATH="${_path}"
}

# add a datastore for iso files
#
# @param string _name the name of the datastore
# @param string _path filesystem path
#
datastore::iso(){
    local _name="$1"
    local _path="$2"

    [ -z "${_name}" -o -z "${_path}" ] && util::usage
    util::check_name "${_name}" || util::err "invalid datastore name - '${_name}'"

    # check name not in use
    for _curr in ${VM_DATASTORE_LIST}; do
        [ "${_curr}" = "${_name}" ] && util::err "datstore '${_name}' already exists!"
    done

    # make sure directory exists
    [ ! -d "${_path}" ] && util::err "specified directory does not appear to be valid"

    # save
    config::core::set "datastore_list" "${_name}" "1"
    config::core::set "path_${_name}" "iso:${_path}"
    [ $? -eq 0 ] || util::err "error saving settings to configuration file"
}

# add a datastore for img files
#
# @param string _name the name of the datastore
# @param string _path filesystem path
#
datastore::img(){
    local _name="$1"
    local _path="$2"

    [ -z "${_name}" -o -z "${_path}" ] && util::usage
    util::check_name "${_name}" || util::err "invalid datastore name - '${_name}'"

    # check name not in use
    for _curr in ${VM_DATASTORE_LIST}; do
        [ "${_curr}" = "${_name}" ] && util::err "datstore '${_name}' already exists!"
    done

    # make sure directory exists
    [ ! -d "${_path}" ] && util::err "specified directory does not appear to be valid"

    # save
    config::core::set "datastore_list" "${_name}" "1"
    config::core::set "path_${_name}" "img:${_path}"
    [ $? -eq 0 ] || util::err "error saving settings to configuration file"
}

# find an iso file by looking in the default location
# and any "iso" datastores
#
# @param string _var variable name to put full iso path into
# @param string _file iso filename to look for
# @return int success if found
#
datastore::iso_find(){
    local _var="$1"
    local _file="$2"
    local _ds _spec

    # given a full path?
    if [ -z "${_file%%/*}" ] && [ -r "${_file}" ]; then
        setvar "${_var}" "${_file}"
        return 0
    fi

    # file exists in current dir?
    if [ -r "$(pwd)/${_file}" ]; then
        setvar "${_var}" "$(pwd)/${_file}"
        return 0
    fi

    # look in default store
    if [ -r "${vm_dir}/.iso/${_file}" ]; then
        setvar "${_var}" "${vm_dir}/.iso/${_file}"
        return 0
    fi

    for _ds in ${VM_DATASTORE_LIST}; do
            config::core::get "_spec" "path_${_ds}"
            [ "${_spec%%:*}" != "iso" ] && continue

            if [ -r "${_spec#*:}/${_file}" ]; then
                setvar "${_var}" "${_spec#*:}/${_file}"
                return 0
            fi
    done

    return 1
}

# find an img file by looking in the default location
# and any "img" datastores
#
# @param string _var variable name to put full img path into
# @param string _file img filename to look for
# @return int success if found
#
datastore::img_find(){
    local _var="$1"
    local _file="$2"
    local _ds _spec

    # given a full path?
    if [ -z "${_file%%/*}" ] && [ -r "${_file}" ]; then
        setvar "${_var}" "${_file}"
        return 0
    fi

    # file exists in current dir?
    if [ -r "$(pwd)/${_file}" ]; then
        setvar "${_var}" "$(pwd)/${_file}"
        return 0
    fi

    # look in default store
    if [ -r "${vm_dir}/.img/${_file}" ]; then
        setvar "${_var}" "${vm_dir}/.img/${_file}"
        return 0
    fi

    for _ds in ${VM_DATASTORE_LIST}; do
            config::core::get "_spec" "path_${_ds}"
            [ "${_spec%%:*}" != "img" ] && continue

            if [ -r "${_spec#*:}/${_file}" ]; then
                setvar "${_var}" "${_spec#*:}/${_file}"
                return 0
            fi
    done

    return 1
}

# list iso files
#
datastore::iso_list(){
    local _ds _spec _format="%-20s%s\n"

    printf "${_format}" "DATASTORE" "FILENAME"

    # look for default iso location
    [ -d "${vm_dir}/.iso" ] && ls -1 "${vm_dir}/.iso" | awk '{printf "'${_format}'","default",$1}'

    # look for iso datastores
    for _ds in ${VM_DATASTORE_LIST}; do
        config::core::get "_spec" "path_${_ds}"
        [ "${_spec%%:*}" != "iso" ] && continue

        [ -d "${_spec#*:}" ] && ls -1 ${_spec#*:} | awk '{printf "'${_format}'","'${_ds}'",$1}'
    done
}

# list img files
#
datastore::img_list(){
    local _ds _spec _format="%-20s%s\n"

    printf "${_format}" "DATASTORE" "FILENAME"

    # look for default img location
    [ -d "${vm_dir}/.img" ] && ls -1 "${vm_dir}/.img" | awk '{printf "'${_format}'","default",$1}'

    # look for img datastores
    for _ds in ${VM_DATASTORE_LIST}; do
        config::core::get "_spec" "path_${_ds}"
        [ "${_spec%%:*}" != "img" ] && continue

        [ -d "${_spec#*:}" ] && ls -1 ${_spec#*:} | awk '{printf "'${_format}'","'${_ds}'",$1}'
    done
}
